/*
 * @Author: xiaosihan
 * @Date: 2022-04-07 16:42:26
 * @Last Modified by: xiaosihan
 * @Last Modified time: 2025-01-15 20:34:19
 */

import {
  CanvasTexture,
  DefaultLoadingManager,
  EventDispatcher,
  Material,
  Mesh,
  MeshStandardMaterial,
  RepeatWrapping,
  SRGBColorSpace,
  Texture,
  TextureLoader,
  VideoTexture,
} from "three";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
import { STLLoader } from "three/examples/jsm/loaders/STLLoader";
import { USDZLoader } from "three/examples/jsm/loaders/USDZLoader";

// import { IFCLoader } from 'web-ifc-three/dist/IFCLoader';
// import { IFCLoader } from 'web-ifc-three';
// import { IFCSPACE } from 'web-ifc';

// 引入静态资源
//@ts-ignore
import dracoWasmWrapperJS from "./lib/draco/draco_wasm_wrapper.js?url";
//@ts-ignore
import dracoDecoderJS from "./lib/draco/draco_decoder.js?url";
//@ts-ignore
import axios from "axios";
import { isArray, isEmpty, isNull, isString, isUndefined } from "lodash";
import { generateUUID } from "three/src/math/MathUtils";
import dracoDecoderWASM from "./lib/draco/draco_decoder.wasm?url";
import threeUtils from "./threeUtils";

import { PLYLoader, TGALoader } from "three/examples/jsm/Addons.js";
DefaultLoadingManager.addHandler(/tga$/, new TGALoader());

// 加载器
class ThreeLoader extends EventDispatcher {
  constructor() {
    super();

    DefaultLoadingManager.onStart = () => {
      this.onload = false;
    };

    // 有资源加载完成时通知渲染器
    DefaultLoadingManager.onProgress = (
      url: string,
      loaded: number,
      total: number
    ) => {
      if (loaded < total) {
        this.dispatchEvent({ type: "loadding" } as never);
      } else if (loaded === total) {
        this.dispatchEvent({ type: "onLoad" } as never);
      }
    };

    DefaultLoadingManager.onLoad = () => {
      this.onload = true;
    };

    // 资源替换
    DefaultLoadingManager.setURLModifier((url) => {
      //全路径匹配资源
      if (this._blobs[url]) {
        return this._blobs[url];
      }
      const filename = url
        .replace(`blob:${location.origin}/`, "")
        .replace(/^\.\//, "");
      let new_url = this._blobs[filename] || url;
      //如果全匹配没有匹配到资源,就尝试文件名匹配
      if (!this._blobs[filename]) {
        const blobKey =
          Object.keys(this._blobs).find(
            (key) => this.getFilename(key) === this.getFilename(filename)
          ) || "";
        new_url = this._blobs[blobKey] || url;
      }
      return new_url;
    });
  }

  // 从url 中提取文件名
  getFilename(url: string) {
    // 移除URL的查询字符串和哈希部分
    const path = url.split("?")[0].split("#")[0];
    // 使用lastIndexOf和substring从路径中提取文件名
    const filename = path.substring(path.lastIndexOf("/") + 1);
    return filename;
  }

  uuid = generateUUID();

  // 各向异性
  anisotropy = 16;

  // 设置各向异性 由渲染器来调用并设置
  setAnisotropy(v: number) {
    this.anisotropy = v;
  }

  // 纹理对向缓存
  textureMap = new Map() as Map<string, Texture>;

  // 纹理加载器
  textureLoader = new TextureLoader();

  // 获取地址对应的单例纹理对象
  getTexture(src: string | HTMLCanvasElement) {
    if (src instanceof HTMLCanvasElement) {
      const canvasTexture = new CanvasTexture(src);
      canvasTexture.wrapS = canvasTexture.wrapT = RepeatWrapping;
      canvasTexture.anisotropy = this.anisotropy;
      canvasTexture.colorSpace = SRGBColorSpace;

      return canvasTexture;
    }

    let texture = this.textureMap.get(src);

    if (!texture) {
      if (/\.(mp4|mov)$/i.test(src)) {
        const videoDom = document.createElement("video");
        videoDom.autoplay = true;
        videoDom.loop = true;
        videoDom.muted = true;
        videoDom.preload = "auto";
        videoDom.playsInline = true;
        videoDom.src = src;
        videoDom.crossOrigin = "anonymous";
        videoDom.play();
        const firstOptoin = () => {
          videoDom.pause();
          videoDom.play();
          document.removeEventListener("pointerup", firstOptoin);
          document.removeEventListener("touchstart", firstOptoin);
        };
        document.addEventListener("pointerup", firstOptoin);
        document.addEventListener("touchstart", firstOptoin);
        texture = new VideoTexture(videoDom);
      } else {
        texture = this.textureLoader.load(src, (tex) => {
          tex.image.crossOrigin = "anonymous";
        });
      }

      this.textureMap.set(src, texture);
    }

    texture.wrapS = texture.wrapT = RepeatWrapping;
    texture.anisotropy = this.anisotropy;
    texture.colorSpace = SRGBColorSpace;

    return texture as Texture;
  }

  // 资源列表
  _blobs: { [key: string]: string } = {};
  // fbx模型加载器
  fbxLoader = new FBXLoader();
  //obj 加载器
  objLoader = new OBJLoader();
  // stl 加载器
  stlLoader = new STLLoader();
  //ply加载器
  plyLoader = new PLYLoader();
  // 材质加载器
  mtlLoader = new MTLLoader();

  // gltf 加载器
  gltfLoader = (() => {
    const loader = new GLTFLoader();
    const dracoLoader = new (class ThreeBaseDRACOLoader extends DRACOLoader {
      _loadLibrary(url: string, responseType: string) {
        //@ts-ignore
        let urlConf =
          process.env.NODE_ENV === "development"
            ? {
                "draco_decoder.js": "draco_decoder.js",
                "draco_wasm_wrapper.js": "draco_wasm_wrapper.js",
                "draco_decoder.wasm": "draco_decoder.wasm",
              }
            : {
                "draco_decoder.js": dracoWasmWrapperJS,
                "draco_wasm_wrapper.js": dracoDecoderJS,
                "draco_decoder.wasm": dracoDecoderWASM,
              };
        //@ts-ignore
        return super._loadLibrary(urlConf[url], responseType);
      }
    })();
    //@ts-ignore
    dracoLoader.setDecoderPath(
      process.env.NODE_ENV === "development"
        ? "/node_modules/three/examples/jsm/libs/draco/"
        : ""
    );
    loader.setDRACOLoader(dracoLoader);
    return loader;
  })();

  //usdz加载器
  usdzLoader = new USDZLoader();

  //把贴图的 flipy 设置为 fasle
  setFlipYIsFalse<T extends Material | Material[]>(material: T) {
    if (material instanceof Array) {
      material.map((m) => this.setFlipYIsFalse(m));
    } else {
      const m = material as MeshStandardMaterial;
      const parame: Array<keyof MeshStandardMaterial> = [
        "map",
        "lightMap",
        "aoMap",
        "emissiveMap",
        "bumpMap",
        "normalMap",
        "displacementMap",
        "alphaMap",
        "envMap",
      ];
      parame.map((param) => {
        const t = m[param] as Texture | undefined;
        if (t) {
          t.flipY = false;
        }
      });
    }
  }

  /** 加载模型支持glb,gltf,fbx,stl,obj,usdz,dae,zip */
  async loadModel(url: string | File | Array<File>, filename: string = "") {
    if (
      (isString(url) || isArray(url)) &&
      isEmpty(url) &&
      isUndefined(url) &&
      isNull(url)
    ) {
      throw new Error(`loadModel:无效的参数 url=${url}`);
    }

    // 本次需要销毁的bloburl,加载完成后就去销毁本次的文件
    const revokeObjectfile: Array<string> = [];

    // 获取模型文件的后缀
    const suffix = await (async () => {
      if (isString(url)) {
        return threeUtils.getSuffix(filename || (url as string));
      } else if (url instanceof File) {
        return threeUtils.getSuffix(filename || url.name);
      } else if (url instanceof Array) {
        let suffix = "";
        const suffixs = [
          "zip",
          "obj",
          "glb",
          "gltf",
          "fbx",
          "dae",
          "stl",
          "ply",
          "usdz",
        ];
        for (let file of url) {
          suffix = threeUtils.getSuffix(file.name);
          if (suffixs.includes(suffix)) {
            break;
          }
        }
        return threeUtils.getSuffix(filename || suffix);
      }
    })();

    //获取模型文件的地址
    const modelurl = await (async () => {
      if (isString(url)) {
        return url;
      } else if (url instanceof File) {
        const bloburl = await threeUtils.fileToBloburl(url);
        this._blobs[url.name] = bloburl;
        revokeObjectfile.push(url.name);
        return bloburl;
      } else if (url instanceof Array) {
        let modelurl = "";
        const suffixs = [
          "zip",
          "obj",
          "glb",
          "gltf",
          "fbx",
          "dae",
          "stl",
          "usdz",
        ];
        for (let file of url) {
          const bloburl = await threeUtils.fileToBloburl(file);
          this._blobs[file.name] = bloburl;
          revokeObjectfile.push(file.name);
          if (suffixs.includes(threeUtils.getSuffix(file.name))) {
            modelurl = file.name;
          }
        }
        return modelurl;
      }
    })();

    switch (suffix) {
      case "zip":
        const JSZip = (await import("jszip")).default;

        let arrayBuffer = isString(url)
          ? await axios
              .get(url, {
                responseType: "arraybuffer",
              })
              .then((res) => res.data)
          : url instanceof File
          ? url
          : url[0];

        return await JSZip.loadAsync(arrayBuffer).then(async (zip) => {
          for (let filename in zip.files) {
            if (!zip.files[filename].dir) {
              // 不读文件夹
              await zip
                .file(filename)!
                .async("blob")
                .then((blob) => {
                  var blobURL = URL.createObjectURL(blob);
                  this._blobs[filename] = blobURL;
                  revokeObjectfile.push(filename);
                });
            }
          }

          let objLoader = new OBJLoader();

          // 如果有mtl文件就加载mtl文件
          let mtl = Object.keys(zip.files).find((filename) =>
            /\.mtl$/.test(filename)
          );
          if (mtl) {
            const materials = await this.mtlLoader.loadAsync(mtl);
            objLoader.setMaterials(materials);
          } else {
            //@ts-ignore
            objLoader.materials = null;
          }

          //如果有obj就加载obj模型
          let obj = Object.keys(zip.files).find((filename) =>
            /\.obj$/.test(filename)
          );
          if (obj) {
            const objObject3d = await objLoader.loadAsync(obj);
            revokeObjectfile.map((filename) => {
              URL.revokeObjectURL(this._blobs[filename]);
              delete this._blobs[filename];
            });
            objObject3d.traverse((obj) => {
              const mesh = obj as Mesh;

              if (mesh.isMesh) {
                // 如果obj没有材质就给默认的材质
                if (!objLoader.materials) {
                  mesh.material = new MeshStandardMaterial({
                    metalness: 0.5,
                    roughness: 0.5,
                  });
                }

                const uv = mesh.geometry.getAttribute("uv");
                if (uv) {
                  for (let i = 0; i < uv.count; i++) {
                    uv.setY(i, 1 - uv.getY(i));
                  }
                }
                // obj只翻转uv不翻转贴图
                this.setFlipYIsFalse(mesh.material);
              }
            });

            return objObject3d;
          }

          //如果有gltf文件就加载gltf文件
          let gltf = Object.keys(zip.files).find((filename) =>
            /\.(gltf|glb)$/.test(filename)
          );
          if (gltf) {
            return await this.loadModel(gltf);
          }

          //如果有dae文件就加载dae文件
          let dae = Object.keys(zip.files).find((filename) =>
            /\.dae$/.test(filename)
          );
          if (dae) {
            return await this.loadModel(dae);
          }
          //如果有fbx文件就加载fbx文件
          let fbx = Object.keys(zip.files).find((filename) =>
            /\.fbx$/.test(filename)
          );
          if (fbx) {
            return await this.loadModel(fbx);
          }

          //如果有usdz文件就加载usdz文件
          let usdz = Object.keys(zip.files).find((filename) =>
            /\.usdz$/.test(filename)
          );
          if (usdz) {
            return await this.loadModel(usdz);
          }

          //如果有stl文件就加载stl文件
          let stl = Object.keys(zip.files).find((filename) =>
            /\.stl$/.test(filename)
          );
          if (stl) {
            return await this.loadModel(stl);
          }

          //如果有ply文件就加载ply文件
          let ply = Object.keys(zip.files).find((filename) =>
            /\.ply$/.test(filename)
          );
          if (ply) {
            return await this.loadModel(ply);
          }

          //如果有zip文件就加载zip文件
          let zipFile = Object.keys(zip.files).find((filename) =>
            /\.zip$/.test(filename)
          );
          if (zipFile) {
            return await this.loadModel(zipFile);
          }

          if ([obj, gltf, dae, fbx, usdz, stl, ply].every((v) => !v)) {
            throw new Error(
              `${url}压缩包里没有obj.glb.gltf.dae.fbx.usde.stl,ply模型文件`
            );
          }
        });

      case "fbx":
        return await threeLoader.fbxLoader
          .loadAsync(modelurl)
          .then((res) => {
            revokeObjectfile.map((filename) => {
              URL.revokeObjectURL(this._blobs[filename]);
              delete this._blobs[filename];
            });
            return res;
          })
          .catch(() => {
            console.log(`${url},加载失败`);
          });

      case "gltf":
      case "glb":
        return await threeLoader.gltfLoader
          .loadAsync(modelurl)
          .then((res) => {
            revokeObjectfile.map((filename) => {
              URL.revokeObjectURL(this._blobs[filename]);
              delete this._blobs[filename];
            });
            res.scene.animations = res.animations;
            return res.scene;
          })
          .catch(() => {
            console.log(`${url},加载失败`);
          });

      case "stl":
        return await threeLoader.stlLoader
          .loadAsync(modelurl)
          .then((geometry) => {
            revokeObjectfile.map((filename) => {
              URL.revokeObjectURL(this._blobs[filename]);
              delete this._blobs[filename];
            });
            return new Mesh(
              geometry,
              new MeshStandardMaterial({
                metalness: 0.5,
                roughness: 0.5,
              })
            );
          })
          .catch(() => {
            console.log(`${url},加载失败`);
          });

      case "ply":
        return await threeLoader.plyLoader
          .loadAsync(modelurl)
          .then(async (geometry) => {
            revokeObjectfile.map((filename) => {
              URL.revokeObjectURL(this._blobs[filename]);
              delete this._blobs[filename];
            });

            // 没有法线就创建法线
            if (!geometry.getAttribute("normal")) {
              geometry.computeVertexNormals();
            }

            //是否需要绕x轴旋转
            const plyNeedRotationX = await new Promise<boolean>(
              async (resolve) => {
                // 读取文件内容
                const res = await axios.get(modelurl);
                // if (res.match(/\b(Blender|binary_little_endian)\b/)) {
                //   resolve(true);
                // } else {
                //   resolve(false);
                // }
              }
            );

            const plyMesh = new Mesh(
              geometry,
              new MeshStandardMaterial({
                metalness: 0.5,
                roughness: 0.5,
                vertexColors: Boolean(geometry.getAttribute("color")),
              })
            );

            if (plyNeedRotationX) {
              plyMesh.rotation.x = -Math.PI / 2;
            }

            return plyMesh;
          })
          .catch(() => {
            console.log(`${url},加载失败`);
          });

      case "usdz":
        return await threeLoader.usdzLoader
          .loadAsync(modelurl)
          .then((res) => {
            revokeObjectfile.map((filename) => {
              URL.revokeObjectURL(this._blobs[filename]);
              delete this._blobs[filename];
            });
            res.traverse((obj) => {
              const mesh = obj as Mesh;
              if (mesh.isMesh) {
                (mesh.material as Material).transparent = true;
              }
            });
            return res;
          })
          .catch((err) => {
            console.log(`${url},加载失败`, err);
          });

      case "obj":
        let objLoader = new OBJLoader();
        // 如果有mtl文件就加载mtl文件
        let mtl = revokeObjectfile.find((filename) => /\.mtl$/.test(filename));
        if (mtl) {
          const materials = await this.mtlLoader.loadAsync(mtl);
          objLoader.setMaterials(materials);
        }
        const objObject3d = await objLoader.loadAsync(modelurl);
        revokeObjectfile.map((filename) => {
          URL.revokeObjectURL(this._blobs[filename]);
          delete this._blobs[filename];
        });
        objObject3d.traverse((obj) => {
          const mesh = obj as Mesh;

          if (mesh.isMesh) {
            // 如果obj没有材质就给默认的材质
            if (!this.objLoader.materials) {
              mesh.material = new MeshStandardMaterial({
                metalness: 0.5,
                roughness: 0.5,
              });
            }

            const uv = mesh.geometry.getAttribute("uv");
            if (uv) {
              for (let i = 0; i < uv.count; i++) {
                uv.setY(i, 1 - uv.getY(i));
              }
            }
            // obj只翻转uv不翻转贴图
            this.setFlipYIsFalse(mesh.material);
          }
        });
        return objObject3d;

      default:
        revokeObjectfile.map((filename) => {
          URL.revokeObjectURL(this._blobs[filename]);
          delete this._blobs[filename];
        });
        if (suffix) {
          console.log(`${suffix},不支持的格式`);
        } else {
          console.log(
            `${url},不支持的格式,请尝试使用 suffix 参数来指定一下格式`
          );
        }
    }
  }

  // 是否加载完成
  onload = true;
}

const threeLoader = new ThreeLoader();
export default threeLoader;
